﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Linq;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Profile;

using ApplicationBoilerplate.DataProvider;

using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Options;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

namespace MVCBootstrap.MongoDB.Web.Profile {

	public class MongoDBProfile : ProfileProvider {
		private IConnection conn;

		private IConnection Connection {
			get {
				if (this.conn == null) {
					this.conn = DependencyResolver.Current.GetService<IConnection>() as Connection;
				}
				return this.conn;
			}
		}

		public override String ApplicationName { get; set; }

		public override void Initialize(String name, NameValueCollection config) {
			if (String.IsNullOrWhiteSpace(name)) {
				throw new ArgumentNullException("name");
			}
			this.ApplicationName = config["applicationName"] ?? HostingEnvironment.ApplicationVirtualPath ?? String.Empty;

			base.Initialize(name, config);
		}

		public override Int32 DeleteInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate) {
			var authenticationQuery = ConvertProfileAuthenticationOptionToMongoQuery(authenticationOption);

			try {
				var query = Query.And(
					Query.Exists("Profile"), authenticationQuery,
					Query.LTE(String.Format("{0}.{1}", "Profile", UserProfileFieldNames.LastActivityDate), userInactiveSinceDate.Ticks));
				var update = Update.Unset("Profile");
				var users = this.Connection.GetCollection<User>().GetCollection();
				var result = users.Update(query, update, UpdateFlags.Multi);
				return (Int32)result.DocumentsAffected;
			}
			catch (MongoSafeModeException e) {
				throw new ApplicationException("", e);
			}
		}

		public override Int32 DeleteProfiles(String[] usernames) {
			if (usernames == null) {
				throw new ArgumentNullException("userNames");
			}
			if (usernames.Length == 0) {
				return 0;
			}

			try {
				var query = Query.And(
					Query.Exists("Profile"),
					Query.In(UserFieldNames.UpperUsername, BsonArray.Create(usernames.AsEnumerable())));
				var update = Update.Unset("Profile");
				var users = this.Connection.GetCollection<User>().GetCollection();
				var result = users.Update(query, update, UpdateFlags.Multi);
				return (Int32)result.DocumentsAffected;
			}
			catch (MongoSafeModeException e) {
				throw new ApplicationException("", e);
			}
		}

		public override Int32 DeleteProfiles(ProfileInfoCollection profiles) {
			if (profiles == null) {
				throw new ArgumentNullException("profiles");
			}
			if (profiles.Count == 0) {
				return 0;
			}

			return DeleteProfiles(profiles.Cast<ProfileInfo>().Select(p => p.UserName).ToArray());
		}

		public override ProfileInfoCollection FindInactiveProfilesByUserName(ProfileAuthenticationOption authenticationOption, String usernameToMatch, DateTime userInactiveSinceDate, Int32 pageIndex, Int32 pageSize, out Int32 totalRecords) {
			if (pageIndex < 0) {
				throw new ArgumentException("pageIndex");
			}
			if (pageSize < 0) {
				throw new ArgumentException("pageSize");
			}

			var query = Query.And(
				Query.Matches(UserFieldNames.UpperUsername, usernameToMatch.ToUpper()),
				Query.LTE(String.Format("{0}.{1}", "Profile", UserProfileFieldNames.LastActivityDate), userInactiveSinceDate));
			var profiles = FindProfiles(authenticationOption, query, SortBy.Null, pageIndex * pageSize, pageSize, out totalRecords);
			return ConvertProfileInfoEnumerableToCollection(profiles);
		}

		public override ProfileInfoCollection FindProfilesByUserName(ProfileAuthenticationOption authenticationOption, String usernameToMatch, Int32 pageIndex, Int32 pageSize, out Int32 totalRecords) {
			if (pageIndex < 0) {
				throw new ArgumentException("pageIndex");
			}
			if (pageSize < 0) {
				throw new ArgumentException("pageSize");
			}

			var query = Query.Matches(UserFieldNames.UpperUsername, usernameToMatch.ToUpper());
			var profiles = FindProfiles(authenticationOption, query, SortBy.Null, pageIndex * pageSize, pageSize, out totalRecords);
			return ConvertProfileInfoEnumerableToCollection(profiles);
		}

		public virtual IEnumerable<ProfileInfo> FindProfiles(ProfileAuthenticationOption authenticationOption, IMongoQuery query, IMongoSortBy sortBy, int skip, int take, out int totalRecords) {
			if (skip < 0) {
				throw new ArgumentException("skip");
			}
			if (take < 0) {
				throw new ArgumentException("take");
			}

			var authenticationQuery = ConvertProfileAuthenticationOptionToMongoQuery(authenticationOption);
			var modifiedQuery = Query.And(Query.Exists("Profile"), authenticationQuery, query);
			var users = this.Connection.GetCollection<User>().GetCollection();
			var matches = users.Find(modifiedQuery).SetSkip(skip).SetLimit(take);
			if (sortBy != null) {
				matches.SetSortOrder(sortBy);
			}
			totalRecords = (Int32)matches.Count();
			return matches.Select(u => new ProfileInfo(
				u.Username,
				u.IsAnonymous,
				u.Profile.LastActivityDate,
				u.Profile.LastUpdateDate,
				u.Profile.ToBson().Length));
		}

		private static ProfileInfoCollection ConvertProfileInfoEnumerableToCollection(IEnumerable<ProfileInfo> profiles) {
			var collection = new ProfileInfoCollection();
			foreach (var p in profiles) {
				collection.Add(p);
			}
			return collection;
		}

		private IMongoQuery ConvertProfileAuthenticationOptionToMongoQuery(ProfileAuthenticationOption authenticationOption) {
			IMongoQuery query;
			switch (authenticationOption) {
				case ProfileAuthenticationOption.Anonymous:
					query = Query.EQ("IsAnonymous", true);
					break;
				case ProfileAuthenticationOption.Authenticated:
					query = Query.EQ("IsAnonymous", false);
					break;
				case ProfileAuthenticationOption.All:
					query = Query.Null;
					break;
				default:
					throw new ArgumentOutOfRangeException("authenticationOption");
			}
			return query;
		}

		public override ProfileInfoCollection GetAllInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate, Int32 pageIndex, Int32 pageSize, out Int32 totalRecords) {
			if (pageIndex < 0) {
				throw new ArgumentException("pageIndex");
			}
			if (pageSize < 0) {
				throw new ArgumentException("pageSize");
			}

			var query = Query.LTE(String.Format("{0}.{1}", "Profile", UserProfileFieldNames.LastActivityDate), userInactiveSinceDate);
			var profiles = FindProfiles(authenticationOption, query, SortBy.Null, pageIndex * pageSize, pageSize, out totalRecords);
			return ConvertProfileInfoEnumerableToCollection(profiles);
		}

		public override ProfileInfoCollection GetAllProfiles(ProfileAuthenticationOption authenticationOption, Int32 pageIndex, Int32 pageSize, out Int32 totalRecords) {
			if (pageIndex < 0) {
				throw new ArgumentException("pageIndex");
			}
			if (pageSize < 0) {
				throw new ArgumentException("pageSize");
			}

			var profiles = FindProfiles(authenticationOption, Query.Null, SortBy.Null, pageIndex * pageSize, pageSize, out totalRecords);
			return ConvertProfileInfoEnumerableToCollection(profiles);
		}

		public override Int32 GetNumberOfInactiveProfiles(ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate) {
			var authenticationQuery = ConvertProfileAuthenticationOptionToMongoQuery(authenticationOption);

			try {
				var query = Query.And(
					Query.Exists("Profile"), authenticationQuery,
					Query.LTE(String.Format("{0}.{1}", "Profile", UserProfileFieldNames.LastActivityDate), userInactiveSinceDate));
				var users = this.Connection.GetCollection<User>().GetCollection();
				return (Int32)users.Count(query);
			}
			catch (MongoSafeModeException e) {
				throw new ApplicationException("", e);
			}
		}

		public static BsonDocument Serialize(Type nominalType, object value, IBsonSerializationOptions options) {
			var document = new BsonDocument();
			var writerSettings = new BsonDocumentWriterSettings();
			var writer = new BsonDocumentWriter(document, writerSettings);
			BsonSerializer.Serialize(writer, nominalType, value, options);
			return document;
		}

		public static BsonDocument Serialize(Type nominalType, object value) {
			return Serialize(nominalType, value, null);
		}

		public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection) {
			if (context == null) {
				throw new ArgumentNullException("context");
			}
			if (collection == null) {
				throw new ArgumentNullException("collection");
			}
			if (collection.Count == 0) {
				return new SettingsPropertyValueCollection();
			}

			SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
			foreach (SettingsProperty p in collection) {
				values.Add(new SettingsPropertyValue(p));
			}

			String userName = (String)context["UserName"];
			if (String.IsNullOrWhiteSpace(userName)) {
				return values;
			}

			UserProfile profile = this.Connection.GetCollection<User>().ReadOne(u => u.UpperUsername == userName.ToUpper()).Profile;
			if (profile == null) {
				return values;
			}

			try {
				var query = Query.EQ(UserFieldNames.UpperUsername, userName.ToUpper());
				var update = Update.Set(String.Format("{0}.{1}", "Profile", UserProfileFieldNames.LastActivityDate), DateTime.UtcNow);
				var users = this.Connection.GetCollection<User>().GetCollection();
				users.Update(query, update);
			}
			catch (WriteConcernException e) {
				throw new ApplicationException("", e);
			}

			var bsonReader = BsonReader.Create(profile.Properties);
			bsonReader.ReadStartDocument();

			while (bsonReader.ReadBsonType() != BsonType.EndOfDocument) {
				var elementName = bsonReader.ReadName();

				var value = values[elementName];
				if (value == null) {
					bsonReader.SkipValue();
					continue;
				}

				switch (value.Property.SerializeAs) {
					case SettingsSerializeAs.String:
					case SettingsSerializeAs.Xml:
						value.SerializedValue = bsonReader.ReadString();
						break;
					case SettingsSerializeAs.Binary:
						byte[] bytes;
						BsonBinarySubType subType;
						bsonReader.ReadBinaryData(out bytes, out subType);
						value.SerializedValue = bytes;
						break;
					case SettingsSerializeAs.ProviderSpecific:
						value.PropertyValue = BsonSerializer.Deserialize(bsonReader, value.Property.PropertyType);
						break;
					default:
						throw new ArgumentOutOfRangeException();
				}
			}

			return values;
		}

		public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection) {
			if (context == null) {
				throw new ArgumentNullException("context");
			}
			if (collection == null) {
				throw new ArgumentNullException("values");
			}

			String username = (String)context["UserName"];
			Boolean isAuthenticated = (Boolean)context["IsAuthenticated"];

			if (String.IsNullOrWhiteSpace(username) || collection.Count == 0) {
				return;
			}

			var updateValues = (from SettingsPropertyValue value in collection
								let allowAnonymous = value.Property.Attributes["AllowAnonymous"].Equals(true)
								where (value.IsDirty || !value.UsingDefaultValue) && (isAuthenticated || allowAnonymous)
								select value).ToList();

			// If there are no values to update, then we're done here.
			if (updateValues.Count == 0) {
				return;
			}

			// If the user doesn't exist, and it's anonymous, create it.
			User user = this.Connection.GetCollection<User>().ReadOne(u => u.UpperUsername == username.ToUpper());
			if (user == null) {
				if (!isAuthenticated) {
					user = new User {
						Username = username,
						UpperUsername = username.ToUpper(),
						IsAnonymous = true,
						CreationDate = DateTime.UtcNow,
					};

					try {
						var users = this.Connection.GetCollection<User>().GetCollection();
						users.Insert(user);
					}
					catch (WriteConcernException e) {
						throw new ApplicationException("", e);
					}
				}
				else {
					throw new ApplicationException();
				}
			}

			// Create the properties BSON document.
			var properties = new BsonDocument();
			var propertiesWriterSettings = new BsonDocumentWriterSettings();
			var propertiesWriter = new BsonDocumentWriter(properties, propertiesWriterSettings);
			propertiesWriter.WriteStartDocument();
			foreach (var value in updateValues) {
				propertiesWriter.WriteName(value.Name);
				switch (value.Property.SerializeAs) {
					case SettingsSerializeAs.String:
					case SettingsSerializeAs.Xml:
						BsonSerializer.Serialize(propertiesWriter, typeof(string), value.SerializedValue);
						break;
					case SettingsSerializeAs.Binary:
						BsonSerializer.Serialize(propertiesWriter, typeof(byte[]), value.SerializedValue);
						break;
					case SettingsSerializeAs.ProviderSpecific:
						BsonSerializer.Serialize(propertiesWriter, value.Property.PropertyType, value.PropertyValue);
						break;
					default:
						throw new ArgumentOutOfRangeException();
				}
			}
			propertiesWriter.WriteEndDocument();

			// Create the profile BSON document.
			var profile = Serialize(typeof(UserProfile), new UserProfile {
				Properties = properties,
				LastActivityDate = DateTime.UtcNow,
				LastUpdateDate = DateTime.UtcNow
			});

			try {
				var query = Query.EQ(UserFieldNames.UpperUsername, username.ToUpper());
				var update = Update.Set("Profile", profile);
				var users = this.Connection.GetCollection<User>().GetCollection();
				users.Update(query, update);
			}
			catch (WriteConcernException e) {
				throw new ApplicationException("", e);
			}
		}
	}
}